<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Hacker News Multi-Term Histogram</title>
        <style>
            /* ---------- Layout & typography ---------- */
            body {
                font-family: system-ui, sans-serif;
                padding: 1rem 2rem;
                max-width: 1024px;
                margin: 0 auto;
            }
            h1 {
                margin-top: 0;
            }
            form {
                display: flex;
                flex-wrap: wrap;
                gap: 0.75rem;
                align-items: flex-end;
                margin-bottom: 1.5rem;
            }
            label {
                display: flex;
                flex-direction: column;
                font-size: 0.9rem;
                min-width: 12ch;
            }
            input[type="text"],
            select {
                padding: 0.4rem 0.6rem;
                border: 1px solid #ccc;
                border-radius: 4px;
                font-size: 1rem;
            }
            button {
                padding: 0.5rem 0.9rem;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-weight: 600;
            }
            #addTermBtn {
                background: #6c757d;
                color: #fff;
            }
            #runBtn {
                background: #ff6600;
                color: #fff;
            }
            #copyJsonBtn {
                background: #198754;
                color: #fff;
                margin-bottom: 0.5rem;
            }

            /* ---------- Chart ---------- */
            .chart {
                display: flex;
                align-items: flex-end;
                gap: 6px;
                height: 320px;
                border: 1px solid #ddd;
                padding: 4px;
                overflow-x: auto;
                overflow-y: visible;
                margin-bottom: 2rem;
                position: relative;
            }
            .bar-group {
                display: flex;
                flex-direction: column;
                align-items: stretch;
                height: 100%;
                overflow: visible;
            }
            .bars {
                display: flex;
                align-items: flex-end;
                gap: 2px;
                flex: 1 0 auto;
                overflow: visible;
            }
            /* wrapper bar occupies full height so hover works even for tiny values */
            .bar {
                width: 10px;
                height: 100%;
                position: relative;
                cursor: pointer;
            }
            /* make wrapper a hover target even when fill height is 0 */
            .bar::before {
                content: attr(data-hits);
                opacity: 0; /* invisible but preserves hover area */
            }
            /* visible coloured portion */
            .fill {
                position: absolute;
                bottom: 0;
                left: 0;
                width: 100%;
                height: 0%; /* updated via JS */
                transition: height 0.3s ease;
            }
            /* tooltip always shows because wrapper is full height */
            .bar:hover::after {
                content: attr(data-term) " (" attr(data-hits) ")";
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: #000;
                color: #fff;
                font-size: 0.75rem;
                padding: 3px 6px;
                border-radius: 3px;
                white-space: nowrap;
                z-index: 10;
                pointer-events: none;
            }
            .bar-label {
                text-align: center;
                font-size: 0.6rem;
                margin-top: 2px;
                user-select: none;
                writing-mode: vertical-rl;
                transform: rotate(180deg);
            }

            /* ---------- Table & JSON ---------- */
            table {
                width: 100%;
                border-collapse: collapse;
                margin-bottom: 1.5rem;
                font-variant-numeric: tabular-nums;
            }
            th,
            td {
                border: 1px solid #ddd;
                padding: 0.4rem 0.6rem;
                text-align: right;
            }
            th {
                text-align: left;
                background: #f5f5f5;
            }
            pre {
                background: #f9f9f9;
                border: 1px solid #eee;
                padding: 0.75rem;
                overflow-x: auto;
                max-height: 320px;
            }
        </style>
    </head>
    <body>
        <h1>Hacker News Multi-Term Histogram</h1>
        <form id="searchForm">
            <div id="termInputs">
                <label>
                    Search term
                    <input
                        type="text"
                        class="term"
                        placeholder="e.g., sqlite"
                    />
                </label>
            </div>
            <button id="addTermBtn" type="button">＋</button>
            <label>Start year<select id="startYear"></select></label>
            <button id="runBtn" type="submit">Run searches</button>
        </form>

        <div id="chart" class="chart"></div>

        <table id="resultsTable">
            <thead></thead>
            <tbody></tbody>
        </table>

        <h3>JSON output</h3>
        <button id="copyJsonBtn" type="button">Copy to clipboard</button>
        <pre id="jsonOutput">[]</pre>

        <script>
            const termInputsDiv = document.getElementById("termInputs");
            const addTermBtn = document.getElementById("addTermBtn");
            const yearSelect = document.getElementById("startYear");
            const chartEl = document.getElementById("chart");
            const tableHead = document.querySelector("#resultsTable thead");
            const tableBody = document.querySelector("#resultsTable tbody");
            const jsonOutput = document.getElementById("jsonOutput");
            const copyBtn = document.getElementById("copyJsonBtn");

            const MAX_TERMS = 5;
            const CONCURRENCY_LIMIT = 6;
            const COLORS = [
                "#ff6600",
                "#0d6efd",
                "#28a745",
                "#6f42c1",
                "#fd7e14",
            ];

            /* populate years */
            const currentYear = new Date().getFullYear();
            for (let y = currentYear; y >= 2015; y--) {
                const opt = document.createElement("option");
                opt.value = y;
                opt.textContent = y;
                if (y === 2020) opt.selected = true;
                yearSelect.appendChild(opt);
            }

            /* add term inputs */
            addTermBtn.addEventListener("click", () => {
                const existing =
                    termInputsDiv.querySelectorAll("input.term").length;
                if (existing >= MAX_TERMS) return;
                const lbl = document.createElement("label");
                lbl.innerHTML =
                    'Search term <input type="text" class="term" />';
                termInputsDiv.appendChild(lbl);
                if (existing + 1 >= MAX_TERMS) addTermBtn.disabled = true;
            });

            /* copy to clipboard */
            copyBtn.addEventListener("click", async () => {
                try {
                    await navigator.clipboard.writeText(jsonOutput.textContent);
                    const orig = copyBtn.textContent;
                    copyBtn.textContent = "Copied!";
                    setTimeout(() => (copyBtn.textContent = orig), 1000);
                } catch (err) {
                    console.error("Clipboard copy failed", err);
                }
            });

            /* helpers */
            const monthLabel = (d) => d.toISOString().slice(0, 7);
            function buildMonths(startYear) {
                const now = new Date();
                const arr = [];
                let dt = new Date(Date.UTC(startYear, 0, 1));
                while (dt <= now) {
                    arr.push(new Date(dt));
                    dt.setUTCMonth(dt.getUTCMonth() + 1);
                }
                return arr;
            }

            function createChartSkeleton(monthLabels, termCount) {
                chartEl.innerHTML = "";
                const barGroups = [];
                for (let i = monthLabels.length - 1; i >= 0; i--) {
                    const group = document.createElement("div");
                    group.className = "bar-group";
                    group.style.width = `${termCount * 12}px`;
                    const barsRow = document.createElement("div");
                    barsRow.className = "bars";
                    for (let t = 0; t < termCount; t++) {
                        const barWrapper = document.createElement("div");
                        barWrapper.className = "bar";
                        const fill = document.createElement("div");
                        fill.className = "fill";
                        fill.style.background = COLORS[t];
                        barWrapper.appendChild(fill);
                        barsRow.appendChild(barWrapper);
                    }
                    const label = document.createElement("div");
                    label.className = "bar-label";
                    label.textContent = monthLabels[i];
                    group.appendChild(barsRow);
                    group.appendChild(label);
                    chartEl.appendChild(group);
                    barGroups[i] = barsRow;
                }
                return barGroups;
            }

            function updateChart(barGroups, results, terms) {
                const maxHits = Math.max(1, ...results.flat());
                results[0].forEach((_, mIdx) => {
                    const row = barGroups[mIdx];
                    terms.forEach((term, tIdx) => {
                        const barWrapper = row.children[tIdx];
                        const fillEl = barWrapper.querySelector(".fill");
                        const hits = results[tIdx][mIdx] ?? 0;
                        fillEl.style.height = `${(hits / maxHits) * 100}%`;
                        barWrapper.dataset.hits = hits;
                        barWrapper.dataset.term = term;
                    });
                });
            }

            function updateTable(monthLabels, terms) {
                tableHead.innerHTML = "";
                const headRow = document.createElement("tr");
                headRow.appendChild(
                    Object.assign(document.createElement("th"), {
                        textContent: "Month",
                    }),
                );
                terms.forEach((t, idx) => {
                    const th = document.createElement("th");
                    th.textContent = t;
                    th.style.color = COLORS[idx];
                    headRow.appendChild(th);
                });
                tableHead.appendChild(headRow);

                tableBody.innerHTML = "";
                for (let idx = monthLabels.length - 1; idx >= 0; idx--) {
                    const label = monthLabels[idx];
                    const tr = document.createElement("tr");
                    const td = document.createElement("td");
                    td.textContent = label;
                    td.style.textAlign = "left";
                    tr.appendChild(td);
                    tableBody.appendChild(tr);
                }
            }

            function updateTableCell(monthIdx, termIdx, value) {
                const revIdx = tableBody.rows.length - 1 - monthIdx; // reversed table order
                const row = tableBody.rows[revIdx];
                while (row.cells.length < termIdx + 2) {
                    row.appendChild(document.createElement("td"));
                }
                row.cells[termIdx + 1].textContent = value.toLocaleString();
            }

            function updateJSON(monthLabels, terms, results) {
                const out = monthLabels.map((label, mIdx) => {
                    const obj = {};
                    terms.forEach((t, tIdx) => {
                        obj[t] = results[tIdx][mIdx] ?? 0;
                    });
                    return { [label]: obj };
                });
                jsonOutput.textContent = JSON.stringify(out, null, 2);
            }

            /* simple concurrency pool */
            async function runWithLimit(tasks, limit) {
                const executing = new Set();
                for (const task of tasks) {
                    const p = task();
                    executing.add(p);
                    const clean = () => executing.delete(p);
                    p.then(clean).catch(clean);
                    if (executing.size >= limit) {
                        await Promise.race(executing);
                    }
                }
                await Promise.all(executing);
            }

            /* main form handler */
            document
                .getElementById("searchForm")
                .addEventListener("submit", (e) => {
                    e.preventDefault();
                    const terms = Array.from(
                        document.querySelectorAll("input.term"),
                    )
                        .map((i) => i.value.trim())
                        .filter(Boolean)
                        .slice(0, MAX_TERMS);
                    if (!terms.length) return;

                    const startYear = parseInt(yearSelect.value, 10);
                    const months = buildMonths(startYear);
                    const monthLabels = months.map(monthLabel);

                    const results = terms.map(() =>
                        Array(months.length).fill(null),
                    );

                    const barGroups = createChartSkeleton(
                        monthLabels,
                        terms.length,
                    );
                    updateTable(monthLabels, terms);
                    updateJSON(monthLabels, terms, results);

                    /* create fetch tasks round-robin from newest month backwards */
                    const tasks = [];
                    for (let offset = 0; offset < months.length; offset++) {
                        const mIdx = months.length - 1 - offset; // reverse chronological
                        terms.forEach((term, tIdx) => {
                            const startDate = months[mIdx];
                            const endDate = new Date(startDate);
                            endDate.setUTCMonth(endDate.getUTCMonth() + 1);
                            const startEpoch = Math.floor(
                                startDate.getTime() / 1000,
                            );
                            const endEpoch = Math.floor(
                                endDate.getTime() / 1000,
                            );

                            const apiUrl = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(term)}&numericFilters=created_at_i>=${startEpoch},created_at_i<${endEpoch}&hitsPerPage=0`;

                            /* click handler (attach once) */
                            const barWrapper = barGroups[mIdx].children[tIdx];
                            if (!barWrapper.dataset.clickAttached) {
                                barWrapper.dataset.clickAttached = "1";
                                barWrapper.addEventListener("click", () => {
                                    const searchUrl = `https://hn.algolia.com/?dateEnd=${endEpoch}&dateRange=custom&dateStart=${startEpoch}&page=0&prefix=false&query=${encodeURIComponent(term)}&sort=byDate&type=story`;
                                    window.open(searchUrl, "_blank");
                                });
                            }

                            tasks.push(async () => {
                                try {
                                    const data = await fetch(apiUrl).then((r) =>
                                        r.json(),
                                    );
                                    const hits = data.nbHits ?? 0;
                                    results[tIdx][mIdx] = hits;
                                    updateTableCell(mIdx, tIdx, hits);
                                    updateChart(barGroups, results, terms);
                                    updateJSON(monthLabels, terms, results);
                                } catch (err) {
                                    console.error(err);
                                    results[tIdx][mIdx] = 0;
                                    updateTableCell(mIdx, tIdx, 0);
                                }
                            });
                        });
                    }

                    runWithLimit(tasks, CONCURRENCY_LIMIT);
                });
        </script>
    </body>
</html>
